package net.carinae.dev.seduku.model;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;

import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Transient;

import net.carinae.dev.seduku.utils.SudokuType.Sudoku4Digit;



/**
 * A 4x4 Sudoku.
 * 
 * @author Carlos Vara
 * @author Patricia Grao
 */
@Entity
public class Sudoku4 implements Serializable {
	
	/**
	 * The representation of the original board.
	 */
	@Basic
	@Id
	private String originalRep;

	/**
	 * The representation of the current board.
	 * Not stored in DB, but serialized by wicket.
	 */
	@Transient
	private String currentRep; 
	
	/**
	 * The representation of the solution to the current sudoku.
	 */
	@Basic
	private String solutionRep;
	
	
	/**
	 * A board in which each square has a set of the possible digits that could
	 * be placed in it.
	 */
	@Transient
	private transient HashMap<String,EnumSet<Sudoku4Digit>> possibilities = new HashMap<String, EnumSet<Sudoku4Digit>>();
	
	
	
	public Sudoku4(String originalRep, String solutionRep) {
		this.originalRep = originalRep;
		this.solutionRep = solutionRep;
		init();
	}
	
	
	/**
	 * Copy constructor
	 * @param orig The original Sudoku4 to be copied.
	 */
	public Sudoku4(Sudoku4 orig) {
		this.originalRep = orig.originalRep;
		this.solutionRep = orig.solutionRep;
		this.currentRep = orig.currentRep;
		init();
	}
	
	
	
	/**
	 * Inits the Sudoku4 non-persisted and transient fields.
	 */
	public void init() {
		
		// Init actualRep (if needed)
		if ( this.currentRep == null || this.currentRep.isEmpty() ) {
			this.currentRep = this.originalRep;
		}
		
		// Init possibilities (iterate the actualRep string, and propagate)
		this.possibilities = new HashMap<String, EnumSet<Sudoku4Digit>>(16, 1.0f);
		for ( String square : SQUARES ) {
			this.possibilities.put(square, EnumSet.allOf(Sudoku4Digit.class));
		}
		for ( int i=0; i<this.currentRep.length(); ++i ) {
			// If currentVal is 1,2,3,4 assign it, if not, ignore
			try {
				Sudoku4Digit digit = Sudoku4Digit.valueOf("d" + this.currentRep.charAt(i));
				assign(SQUARES[i], digit);
			} catch (IllegalArgumentException e) {
				continue;
			}
		}
		
	}


	/**
	 * Assigns a digit 'd' in the indicated square.
	 * 
	 * PRE: This method is only called when the digit can be legally assigned
	 * to that square.
	 * 
	 * @param square The index of the square.
	 * @param d The digit to assign.
	 */
	private void assign(String square, Sudoku4Digit d) {
		this.possibilities.put(square, EnumSet.of(d));
		for ( String peerSquare : peers(square)) {
			this.possibilities.get(peerSquare).remove(d);
		}
	}
	
	
	/**
	 * @return True if the sudoku has been solved, False otherwise. 
	 */
	public boolean isSolved() {
		return this.currentRep.equalsIgnoreCase(this.solutionRep);
	}
	
	
	/**
	 * Gets the string representation of the 'square'.
	 * @param square Index of the square whose representation is requested.
	 * @return The digit if one is assigned, empty string if none is.
	 */
	public String getCurrentSquareRep(String square) {
		int idx = Arrays.binarySearch(SQUARES, square);
		try {
			Sudoku4Digit digit = Sudoku4Digit.valueOf("d" + this.currentRep.charAt(idx));
			return digit.toString();
		} catch (IllegalArgumentException e) {
			return "";
		}
	}
	
	
	/**
	 * Calculate the next square which have to be completed.
	 * @return the message to indicate user to help him
	 */
	public HashMap<String,ArrayList<Object[]>> nextSquareHelp() {
		
		HashMap<String,ArrayList<Object[]>> ret = new HashMap<String, ArrayList<Object[]>>();
		
		for ( String square : SQUARES ) {
			EnumSet<Sudoku4Digit> squarePossibilites = this.possibilities.get(square);
			// If the digit is alone in the possibilities, it's the next square to set it
			if ( squarePossibilites.size() == 1 && isEnabled(square)) {
				ret.put(square, new ArrayList<Object[]>());
				Object[] toAdd = new Object[1];
				toAdd[0] = squarePossibilites.toArray()[0];		
				ret.get(square).add(toAdd);
				return ret;
			}//it can be extended the help advice considering the posible square numbers minus
			//restricted
		}
			
		return ret;
		
	}
	
	/**
	 * Checks whether the given digit 'd' can be assigned to the square 'square'.
	 * @param square The index of the square.
	 * @param d The digit to check.
	 * @return An empty array if it can be assigned, or an array with reasons
	 * why it can not be assigned.
	 */
	private ArrayList<String> checkSquare(String square, Sudoku4Digit d) {
		
		ArrayList<String> ret = new ArrayList<String>();
		
		// If the digit is alone in the possibilities, it's ok to set it
		EnumSet<Sudoku4Digit> squarePossibilites = this.possibilities.get(square); 
		if ( squarePossibilites.contains(d)) {
			if ( squarePossibilites.size() == 1 ) {
				return ret;
			}
			else {
				ret.add("not.unique");
				return ret;
			}
		}
		else {
			
			String[][] squareUnits = units(square);
			
			// Col
			for ( String colIdx : squareUnits[0]) {
				if ( !colIdx.equalsIgnoreCase(square) ) {
					if ( getCurrentSquareRep(colIdx).equalsIgnoreCase(d.toString())) {
						ret.add("already.in.column");
						break;
					}
				}
			}			
			
			// Row
			for ( String colIdx : squareUnits[1]) {
				if ( !colIdx.equalsIgnoreCase(square) ) {
					if ( getCurrentSquareRep(colIdx).equalsIgnoreCase(d.toString())) {
						ret.add("already.in.row");
						break;
					}
				}
			}
			
			// Box
			for ( String colIdx : squareUnits[2]) {
				if ( !colIdx.equalsIgnoreCase(square) ) {
					if ( getCurrentSquareRep(colIdx).equalsIgnoreCase(d.toString())) {
						ret.add("already.in.box");
						break;
					}
				}
			}
			
			return ret;
		}
		
	}
	
	
	/**
	 * Sets the digit 'd' in the square 'square' of this Sudoku.
	 * @param square The index of the square.
	 * @param d The digit to set.
	 */
	private void setSquare(String square, Sudoku4Digit d) {
		
		if ( !checkSquare(square, d).isEmpty() ) {
			// TODO: Maybe in the future allow for incorrect digits to be set?
			throw new IllegalArgumentException("Can't set that value");
		}
		
		// Set the value in currentRep
		int idx = Arrays.binarySearch(SQUARES, square);
		char[] currentChars = this.currentRep.toCharArray();
		currentChars[idx] = d.toString().toCharArray()[0];
		this.currentRep = new String(currentChars);
		
		// Update the possibilities
		assign(square, d);
		
	}
	
	
	public HashMap<String,ArrayList<Object[]>> applyChanges(Sudoku4Changes changes) {
		
		// TODO: Current version only allows setting new values, changing
		// a set value (original or not) is not allowed
		// Because of this, setting an incorrect value is also not allowed
		
		HashMap<String,ArrayList<Object[]>> errors = new HashMap<String, ArrayList<Object[]>>();
		
		HashMap<String,String> cl = changes.getListOfChanges();
		
		// Get the list of indexes whose proposed change is != null
		ArrayList<String> indexes = new ArrayList<String>();
		for ( String square : SQUARES ) {
			if ( cl.get(square) != null ) {
				indexes.add(square);
			}
		}
		
		// Clean the values
		ArrayList<String> tmpIndexes = new ArrayList<String>(indexes);
		for ( String index : indexes ) {
			
			// No change for a currently set value
			if ( !getCurrentSquareRep(index).isEmpty() ) {
				// Silent ignore
				tmpIndexes.remove(index);
				continue;
			}
			
			// Only valid values allowed
			try {
				Sudoku4Digit.valueOf("d"+cl.get(index));
			} catch (IllegalArgumentException e) {
				addError(errors, index, "illegal.value", cl.get(index));
				tmpIndexes.remove(index);
				continue;
			}

		}
		indexes = new ArrayList<String>(tmpIndexes);
		
		// Try to apply the changes until nothing more can be done
		int setValues;
		do {
			setValues = 0;
			ArrayList<String> tmpIndexes2 = new ArrayList<String>(indexes);
			for ( String index : indexes ) {
				Sudoku4Digit d = Sudoku4Digit.valueOf("d"+cl.get(index));
				if (checkSquare(index, d).isEmpty()) {
					setSquare(index, d);
					setValues++;
					tmpIndexes2.remove(index);
				}
			}
			indexes = new ArrayList<String>(tmpIndexes2);
			
		} while (setValues > 0);
		
		// Record last errors, if any
		for ( String index : indexes ) {
			Sudoku4Digit d = Sudoku4Digit.valueOf("d"+cl.get(index));
			for ( String error : checkSquare(index, d) ) {
				addError(errors, index, error, cl.get(index));
			}
		}
		
		return errors;
	}
	
	
	private void addError(HashMap<String,ArrayList<Object[]>> errors, String index, String value, Object... params) {
		
		if ( null == errors.get(index)) {
			errors.put(index, new ArrayList<Object[]>());
		}
		
		Object[] toAdd = new Object[params.length+1];
		toAdd[0] = value;
		for ( int i=1; i<toAdd.length; ++i ) {
			toAdd[i] = params[i-1];
		}
		
		errors.get(index).add(toAdd);
		
	}
	
	
	/**
	 * @see "http://java.sun.com/j2se/1.4.2/docs/api/java/io/Serializable.html"
	 * @param in
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		this.init();
	}
	
	
	/**
	 * All the rows indexes, one character per each.
	 */
	public static final String ROWS = "ABCD";
	
	
    /**
     * All the columns indexes, one character per each.
     */
    public static final String COLS = "1234";
    
    
    /**
     * All the squares indexes, each is a combination of row and column indexes.
     * It's sorted, so it can be used as haystack in a binary search.
     */
    public final static String[] SQUARES = cross(ROWS, COLS);
    
    
    /**
     * Cross product of the characters in each String.
     * @param a First array of characters, as a string.
     * @param b Second array of characters, as a string
     * @return The result of the cross product, an array of 2 char strings.
     */
    public static String[] cross(String a, String b) {

        String[] ret = new String[a.length() * b.length()];
        for (int ia = 0; ia < a.length(); ia++) {
            for (int ib = 0; ib < b.length(); ib++) {
                ret[ia * b.length() + ib] = "" + a.charAt(ia) + b.charAt(ib);
            }
        }

        return ret;
    }
    
    
	/**
	 * Returns the indexes of the squares in each of the units of a square.
	 * There are three kind of units: column, row and box.
	 * @param square Square of which the units are requested.
	 * @return An array of the 3 units, in column, row and box order.
	 */
	public static String[][] units(String square) {

        String[] col = cross(ROWS, "" + square.charAt(1));
        String[] row = cross("" + square.charAt(0), COLS);
        String bRow = (-1 != "AB".indexOf("" + square.charAt(0))) ? "AB" : "CD";
        String bCol = (-1 != "12".indexOf("" + square.charAt(1))) ? "12" : "34";
        String[] box = cross(bRow, bCol);

        return new String[][] { col, row, box };
    }
	
	
    /**
     * Returns the indexes for all the peers of a square.
     * @param square Square of which the peers are requested.
     * @return All the squares that are peers of square.
     */
    public static Set<String> peers(String square) {
        TreeSet<String> ret = new TreeSet<String>();
        for ( String[] unit : units(square) ) {
        	for ( String idx : unit ) {
        		ret.add(idx);
        	}
        }
        ret.remove(square);
        return ret;
    }



	/**
	 * @param inputId The index of the square.
	 * @return True if the square can be edited by the user, false otherwise.
	 */
	public boolean isEnabled(String inputId) {
		return this.getCurrentSquareRep(inputId).isEmpty();
	}
	
	
}
